/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.module.cassandra.sink;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cassandra.core.WriteOptions;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.config.SpelExpressionConverterConfiguration;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.cassandra.outbound.CassandraMessageHandler;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.handler.AbstractMessageProducingHandler;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
import org.springframework.integration.support.json.JsonObjectMapper;
import org.springframework.integration.transformer.AbstractPayloadTransformer;
import org.springframework.integration.transformer.MessageTransformingHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.util.StringUtils;
import org.springframework.cloud.stream.module.cassandra.CassandraConfiguration;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.StdDateFormat;
/**
* @author Artem Bilan
* @author Thomas Risberg
* @author Ashu Gairola
*/
@Configuration
@Import({SpelExpressionConverterConfiguration.class, CassandraConfiguration.class})
@EnableBinding(Sink.class)
@EnableConfigurationProperties(CassandraSinkProperties.class)
public class CassandraSinkConfiguration {
@Autowired
private CassandraSinkProperties cassandraSinkProperties;
@Autowired
private CassandraOperations template;
@Bean
public MessageChannel toSink() {
return new DirectChannel();
}
@Bean
@Primary
@ServiceActivator(inputChannel = Sink.INPUT)
public MessageHandler bridgeMessageHandler() {
AbstractMessageProducingHandler messageHandler;
if (StringUtils.hasText(this.cassandraSinkProperties.getIngestQuery())) {
messageHandler = new MessageTransformingHandler(
new PayloadToMatrixTransformer(this.cassandraSinkProperties.getIngestQuery()));
}
else {
messageHandler = new BridgeHandler();
}
messageHandler.setOutputChannel(toSink());
return messageHandler;
}
@Bean
@ServiceActivator(inputChannel = "toSink")
public MessageHandler cassandraSinkMessageHandler() {
CassandraMessageHandler<?> cassandraMessageHandler =
this.cassandraSinkProperties.getQueryType() != null
? new CassandraMessageHandler<>(this.template, this.cassandraSinkProperties.getQueryType())
: new CassandraMessageHandler<>(this.template);
cassandraMessageHandler.setProducesReply(false);
if (this.cassandraSinkProperties.getConsistencyLevel() != null
|| this.cassandraSinkProperties.getRetryPolicy() != null
|| this.cassandraSinkProperties.getTtl() > 0) {
cassandraMessageHandler.setWriteOptions(
new WriteOptions(this.cassandraSinkProperties.getConsistencyLevel(),
this.cassandraSinkProperties.getRetryPolicy(), this.cassandraSinkProperties.getTtl()));
}
if (StringUtils.hasText(this.cassandraSinkProperties.getIngestQuery())) {
cassandraMessageHandler.setIngestQuery(this.cassandraSinkProperties.getIngestQuery());
}
else if (this.cassandraSinkProperties.getStatementExpression() != null) {
cassandraMessageHandler.setStatementExpression(this.cassandraSinkProperties.getStatementExpression());
}
return cassandraMessageHandler;
}
private static boolean isUuid(String uuid) {
if (uuid.length() == 36) {
String[] parts = uuid.split("-");
if (parts.length == 5) {
if ((parts[0].length() == 8) && (parts[1].length() == 4) &&
(parts[2].length() == 4) && (parts[3].length() == 4) &&
(parts[4].length() == 12)) {
return true;
}
}
}
return false;
}
private static class PayloadToMatrixTransformer extends AbstractPayloadTransformer<Object, List<List<Object>>> {
private static final Pattern PATTERN = Pattern.compile(".+\\((.+)\\).+(?:values\\s*\\((.+)\\))");
private final ObjectMapper objectMapper = new ObjectMapper();
private final JsonObjectMapper<?, ?> jsonObjectMapper = new Jackson2JsonObjectMapper(this.objectMapper);
private final List<String> columns = new LinkedList<>();
private final ISO8601StdDateFormat dateFormat = new ISO8601StdDateFormat();
public PayloadToMatrixTransformer(String query) {
Matcher matcher = PATTERN.matcher(query);
if (matcher.matches()) {
String[] columns = StringUtils.delimitedListToStringArray(matcher.group(1), ",", " ");
String[] params = StringUtils.delimitedListToStringArray(matcher.group(2), ",", " ");
for (int i = 0; i < columns.length; i++) {
String param = params[i];
if (param.equals("?")) {
this.columns.add(columns[i]);
}
else if (param.startsWith(":")) {
this.columns.add(param.substring(1));
}
}
}
else {
throw new IllegalArgumentException("Invalid CQL insert query syntax: " + query);
}
this.objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
@Override
@SuppressWarnings("unchecked")
protected List<List<Object>> transformPayload(Object payload) throws Exception {
if (payload instanceof List) {
return (List<List<Object>>) payload;
}
else {
List<Map<String, Object>> model = this.jsonObjectMapper.fromJson(payload, List.class);
List<List<Object>> data = new ArrayList<>(model.size());
for (Map<String, Object> entity : model) {
List<Object> row = new ArrayList<>(this.columns.size());
for (String column : this.columns) {
Object value = entity.get(column);
if (value instanceof String) {
String string = (String) value;
if (this.dateFormat.looksLikeISO8601(string)) {
synchronized (this.dateFormat) {
value = this.dateFormat.parse(string);
}
}
if (isUuid(string)) {
value = UUID.fromString(string);
}
}
row.add(value);
}
data.add(row);
}
return data;
}
}
}
@SuppressWarnings("serial")
/*
* We need this to provide visibility to the protected method.
*/
private static class ISO8601StdDateFormat extends StdDateFormat {
@Override
protected boolean looksLikeISO8601(String dateStr) {
return super.looksLikeISO8601(dateStr);
}
}
}